﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Data;
using System.Configuration;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml.Linq;
using IList=System.Collections.IList;

namespace InteSoft.Web.ExternalResourceLoader
{
    internal delegate string ExternalResourceReference(string filename, string mimeType, string attributes);

    public static class Extensions
    {
        private static readonly ConfigurationSection section;
        private static readonly SHA1 sha;

        static Extensions()
        {
            section = ConfigurationManager.GetSection("externalResources") as ConfigurationSection;
            sha = new SHA1Managed();
        }

        public static string ExContent(this UrlHelper urlHelper, string contentPath)
        {
            var url = urlHelper.Content(contentPath);
            if (section.Schemes.Count == 0)
            {
                // no external domain specified, leave url as it is
                return url;
            }

            var currentScheme = urlHelper.RequestContext.HttpContext.Request.Url.Scheme;
            var scheme = section.Schemes[currentScheme];

            // if there is no domain config for the current scheme or 
            // no external content roots specified then leave url as it is
            if (scheme == null || scheme.Contents.Count == 0)
                return url;

            Uri baseUri;

            // if there are multiple domains for external content then share requests between them otherwise use root domain
            switch (scheme.Contents.Count)
            {
                case 1:
                    // only one specified ... use it (no need for distributed reference)
                    baseUri = new Uri(currentScheme + Uri.SchemeDelimiter + VirtualPathUtility.AppendTrailingSlash(scheme.Contents[0].ContentRoot));
                    break;
                default:
                    // get a distributed hash key to consistently point to an available resource
                    // (all the content is loadable from any but caching is more efficient if it isn't just random)

                    // Create a SHA1 hash of the provided key
                    byte[] data = BitConverter.GetBytes(contentPath.GetHashCode());
                    byte[] result = sha.ComputeHash(data);

                    // Split the hash into 4 integer numbers
                    uint n1 = BitConverter.ToUInt32(result, 0);
                    uint n2 = BitConverter.ToUInt32(result, 4);
                    uint n3 = BitConverter.ToUInt32(result, 8);
                    uint n4 = BitConverter.ToUInt32(result, 12);
                    uint n5 = BitConverter.ToUInt32(result, 16);

                    // Apply XOR to derive a combined number
                    uint index = n1 ^ n2 ^ n3 ^ n4 ^ n5;

                    // Calculate the bucket index
                    long pageSize = Convert.ToInt64(Math.Ceiling((double) (uint.MaxValue/scheme.Contents.Count)));
                    int contentIndex = Convert.ToInt32(Math.Floor((double) (index / pageSize)));
                    
                    baseUri = new Uri(currentScheme + Uri.SchemeDelimiter + VirtualPathUtility.AppendTrailingSlash(scheme.Contents[contentIndex].ContentRoot));
                    break;
            }
            
            Uri uri = new Uri(baseUri, (contentPath.StartsWith("~/") ? contentPath.Substring(2) : contentPath));
            url = uri.AbsoluteUri;

            return url;
        }

        public static string ExternalResources(this UrlHelper urlHelper, string name)
        {
            ReferenceElement settings = section.References[name];
            if (settings == null)
                throw new ConfigurationErrorsException(string.Format("External reference name {0} not found", name));

            VirtualPathData virtualPathData = RouteTable.Routes.GetVirtualPath(urlHelper.RequestContext, new RouteValueDictionary(new { controller = "ExternalResource", action = "Index", name = settings.Name, version = section.Version, display = Action.Show }));
            string url;

            string currentScheme = urlHelper.RequestContext.HttpContext.Request.Url.Scheme;
            SchemeElement scheme = section.Schemes[currentScheme];

            // if scheme doesn't exist or resourceRoot isn't set ...
            if (scheme == null || String.IsNullOrEmpty(scheme.ResourceRoot))
            {
                // ... then use app relative url
                url = virtualPathData.VirtualPath;
            }
            else
            {
                // ... otherwise, use absolute reference
                string virtualPath = VirtualPathUtility.MakeRelative("~/", virtualPathData.VirtualPath);

                Uri baseUri = new Uri(currentScheme + Uri.SchemeDelimiter + VirtualPathUtility.AppendTrailingSlash(scheme.ResourceRoot));
                Uri uri = new Uri(baseUri, virtualPath);

                url = uri.AbsoluteUri;
            }

            return url;
        }

        public static string ExternalResources(this HtmlHelper htmlHelper, string name)
        {
            return ExternalResources(htmlHelper, name, null);
        }

        public static string ExternalResources(this HtmlHelper htmlHelper, string name, object htmlAttributes)
        {
            return ExternalResources(htmlHelper, name, new RouteValueDictionary(htmlAttributes));
        }

        public static string ExternalResources(this HtmlHelper htmlHelper, string name, RouteValueDictionary htmlAttributes)
        {
            ReferenceElement settings = section.References[name];
            if (settings == null)
                throw new ConfigurationErrorsException(string.Format("External reference name {0} not found", name));
            
            // get distinct show if / hide if conditions and group into files
            IDictionary<Condition, IList<FileInfo>> conditions = new Dictionary<Condition, IList<FileInfo>>(new ConditionComparer());
            foreach (FileInfo fileInfo in settings.Files)
            {
                Condition condition = new Condition{ Action = fileInfo.Action, If = fileInfo.If };
                if (conditions.Keys.Contains(condition)) {
                    conditions[condition].Add(fileInfo);
                } else {
                    conditions.Add(condition, new List<FileInfo>{ fileInfo });
                }
            }

            string attributes = CreateAttributeList(htmlAttributes);

            switch (settings.MimeType)
            {
                case "text/x-javascript":
                case "text/javascript":
                case "text/ecmascript":
                    {
                        ExternalResourceReference formatter = (filename, mimeType, attribs) => string.Format("<script src=\"{0}\" type=\"{1}\"{2}></script>", HttpUtility.HtmlAttributeEncode(filename), settings.MimeType, attribs);
                        return OutputReferences(htmlHelper.ViewContext, conditions, settings, attributes, formatter);
                    }
                case "text/css":
                    {
                        ExternalResourceReference formatter = (filename, mimeType, attribs) => string.Format("<link rel=\"Stylesheet\" href=\"{0}\" type=\"{1}\"{2} />", HttpUtility.HtmlAttributeEncode(filename), settings.MimeType, attribs);
                        return OutputReferences(htmlHelper.ViewContext, conditions, settings, attributes, formatter);
                    }

                // TODO: Decide any other reference types that we want to handle e.g. images
                default:
                    {
                        return string.Empty;
                    }
            }
        }

        private static string OutputReferences(ViewContext viewContext, IDictionary<Condition, IList<FileInfo>> conditions, ReferenceElement settings, string attributes, ExternalResourceReference formatter)
        {
            StringBuilder sb = new StringBuilder();
            foreach (KeyValuePair<Condition, IList<FileInfo>> keyValuePair in conditions)
            {
                Condition condition = keyValuePair.Key;
                IList<FileInfo> files = keyValuePair.Value;

                // we need to wrap these files in conditional comments if the condition is anything but Action = Show with empty if (i.e. show to everything)
                bool conditionalCommentRequired = (!condition.Action.Equals(Action.Show) || !string.IsNullOrEmpty(condition.If));

                if (conditionalCommentRequired)
                {
                    if (condition.Action.Equals(Action.Hide))
                    {
                        sb.AppendFormat("<!--[if {0}]>", condition.If);
                        sb.AppendLine();
                    }
                    else
                    {
                        sb.AppendFormat("<!--[if {0}]>-->", condition.If);
                        sb.AppendLine();
                    }
                }

                switch (section.Mode)
                {
                    case Mode.Release:
                        VirtualPathData virtualPathData = RouteTable.Routes.GetVirtualPath(viewContext.RequestContext, new RouteValueDictionary(new { controller = "ExternalResource", action = "Index", name = settings.Name, version = section.Version, display = condition.Action, condition = condition.If }));
                        string url;

                        string currentScheme = viewContext.HttpContext.Request.Url.Scheme;
                        SchemeElement scheme = section.Schemes[currentScheme];

                        // if scheme doesn't exist or resourceRoot isn't set ...
                        if (scheme == null || String.IsNullOrEmpty(scheme.ResourceRoot)) {
                            // ... then use app relative url
                            url = virtualPathData.VirtualPath;
                        } else {
                            // ... otherwise, use absolute reference
                            string virtualPath = VirtualPathUtility.MakeRelative("~/", virtualPathData.VirtualPath);

                            Uri baseUri = new Uri(currentScheme + Uri.SchemeDelimiter + VirtualPathUtility.AppendTrailingSlash(scheme.ResourceRoot));
                            Uri uri = new Uri(baseUri, virtualPath);

                            url = uri.AbsoluteUri;
                        }

                        sb.AppendLine(formatter.Invoke(url, settings.MimeType, attributes));
                        break;
                    case Mode.Debug:
                        foreach (FileInfo fileInfo in files)
                        {
                            sb.AppendLine(formatter.Invoke(VirtualPathUtility.ToAbsolute(fileInfo.Filename), settings.MimeType, attributes));
                        }
                        break;
                }

                if (conditionalCommentRequired)
                {
                    if (condition.Action.Equals(Action.Hide))
                    {
                        sb.AppendFormat("<![endif]-->");
                        sb.AppendLine();
                    }
                    else
                    {
                        sb.AppendFormat("<!--<![endif]-->");
                        sb.AppendLine();
                    }
                }
            }

            return sb.ToString();
        }
        
        private static string CreateAttributeList(RouteValueDictionary attributes)
        {
            StringBuilder builder = new StringBuilder();
            if (attributes != null)
            {
                foreach (string str in attributes.Keys)
                {
                    string str2 = attributes[str].ToString();
                    if (attributes[str] is bool)
                    {
                        str2 = str2.ToLowerInvariant();
                    }
                    builder.AppendFormat(" {0}=\"{1}\"", str.ToLowerInvariant().Replace("_", ""), str2);
                }
            }
            return builder.ToString();
        }
    }

    internal class ConditionComparer : IEqualityComparer<Condition>
    {
        public bool Equals(Condition x, Condition y)
        {
            return x.Action.Equals(y.Action) && x.If.Equals(y.If);
        }

        public int GetHashCode(Condition obj)
        {
            return obj.Action.GetHashCode() + obj.If.GetHashCode();
        }
    }
}
